package com.chrisshayan.commons;

import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.Predicate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;


/**
 * This class is an utility class for scanning annotation.
 * <br>
 *
 * @author  "Andy Ng" <ckiatng@gmail.com>,
 * @author  ChrisShayan.com
 */
public class AnnotationUtils {
	private static final Log LOG = LogFactory.getLog("AnnotationUtils");

	private static final String CLASS_EXTENSION = ".class";
	private static final String JAR_EXTENSION = ".jar";

	private Class<?> seedClass = null;
	private List<Class<?>> loadedClasses = new ArrayList<Class<?>>();

	/**
	 * @param packageName the package that you are looking to load classes
	 * @throws IOException
	 * @throws ClassNotFoundException
	 */
	public AnnotationUtils(final String packageName) throws IOException, ClassNotFoundException {
		this.loadedClasses = getClasses(packageName);
	}

	/**
	 * @param packageName the package that you are looking to load classes
	 * @param seedClass is a class sitting in a jar file, which hint the util class to load the classes under the package.
	 * @throws IOException
	 * @throws ClassNotFoundException
	 */
	public AnnotationUtils(final String packageName, Class<?> seedClass) throws IOException, ClassNotFoundException {
		this.seedClass = seedClass;
		File file = null;
		try {
			file = new File(this.seedClass.getProtectionDomain().getCodeSource().getLocation().toURI());
		} catch (URISyntaxException e) {
			LOG.error("", e);
		}

		if (file != null && isJarFile(file)) {
			this.loadedClasses = getClasses(packageName, file);
		}

		this.loadedClasses.addAll(getClasses(packageName));
	}

	/**
	 * this method will find your specific annotated class that you might like it
	 *
	 * @param <T>
	 * @param annotation the annotation object
	 * @return list of the classes who is presenting the annotation of T
	 */
	@SuppressWarnings("unchecked")
	public <T extends Annotation> List<Class<?>> findAnnotatedClasses(final Class<T> annotation) {
		return new ArrayList<Class<?>>(CollectionUtils.select(loadedClasses, new Predicate() {
			public boolean evaluate(Object clazz) {
				Class<?> type = (Class<?>) clazz;
				return type.isAnnotationPresent(annotation);
			}
		}));
	}

	/**
	 * This method will find classes that has methods annotated with annotation from parameter.
	 *
	 * @param <T>
	 * @param methodAnnotation
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public <T extends Annotation> List<Class<?>> findClassesWithAnnotatedMethods(final Class<T> methodAnnotation) {
		return new ArrayList<Class<?>>(CollectionUtils.select(loadedClasses, new Predicate() {
			public boolean evaluate(Object clazz) {
				Class<?> type = (Class<?>) clazz;
				Method[] methods = type.getMethods();
				for (Method method : methods) {
					if (method.isAnnotationPresent(methodAnnotation)) {
						return true;
					}
				}
				return false;
			}

		}));
	}

	/**
	 * @param clazz the class that will have the methods
	 * @param annotation the annotation which you are looking into it
	 * @param <T> the annotation object
	 * @return
	 */
	public static <T extends Annotation> List<Method> findAnnotatedMethods(Class<?> clazz, Class<T> annotation) {
		List<Method> annotatedMethods = new ArrayList<Method>();
		Method[] methods = clazz.getMethods();
		for (Method method : methods) {
			if (method.isAnnotationPresent(annotation))
				annotatedMethods.add(method);
		}

		return annotatedMethods;
	}

	/**
	 * Scans all classes in the jar file which belong to the given package and subpackages.
	 *
	 * @param packageName
	 * @param file
	 * @return
	 * @throws IOException
	 * @throws ClassNotFoundException
	 */
	private List<Class<?>> getClasses(String packageName, File file) throws IOException, ClassNotFoundException {
		final List<Class<?>> classes = new ArrayList<Class<?>>();
			final JarFile jarFile = new JarFile(file);
			final Enumeration<JarEntry> jarEntries = jarFile.entries();

			while (jarEntries.hasMoreElements()) {
				final JarEntry jarEntry = jarEntries.nextElement();
				final String entryName = jarEntry.getName();

				if (entryName.endsWith(CLASS_EXTENSION)) {
					String entryFullQualifiedName = getClassFileNameWithoutExtension(entryName).replace("/", ".");

					if (entryFullQualifiedName.startsWith(packageName)) {
						classes.add(Class.forName(entryFullQualifiedName));
					}
				}
			}

			jarFile.close();

		return classes;
	}

	/**
	 * Scans all classes accessible from the context class loader which belong to the given package and subpackages.
	 *
	 * @param packageName The base package
	 * @return The classes
	 * @throws ClassNotFoundException
	 * @throws IOException
	 */
	private List<Class<?>> getClasses(String packageName) throws ClassNotFoundException, IOException {
		ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
		String path = packageName.replace('.', '/');
		Enumeration<URL> resources = classLoader.getResources(path);
		List<File> dirs = new ArrayList<File>();
		while (resources.hasMoreElements()) {
			URL resource = resources.nextElement();
			dirs.add(new File(resource.getFile()));
		}
		List<Class<?>> classes = new ArrayList<Class<?>>();
		for (File directory : dirs) {
			classes.addAll(findClasses(directory, packageName));
		}
		return classes;
	}

	/**
	 * Recursive method used to find all classes in a given directory and subdirs.
	 *
	 * @param directory The base directory
	 * @param packageName The package name for classes found inside the base directory
	 * @return The classes
	 * @throws ClassNotFoundException
	 */
	private List<Class<?>> findClasses(File directory, String packageName) {
		List<Class<?>> classes = new ArrayList<Class<?>>();
		if (!directory.exists()) {
			return classes;
		}
		File[] files = directory.listFiles();
		for (File file : files) {
			if (file.isDirectory()) {
				classes.addAll(findClasses(file, packageName + "." + file.getName()));
			} else {
				if (file.getName().endsWith(CLASS_EXTENSION)) {
					String fullQualifiedClassName = packageName + '.' + getClassFileNameWithoutExtension(file.getName());
					try {
						classes.add(Class.forName(fullQualifiedClassName));
					} catch (ClassNotFoundException e) {
						LOG.warn("Unable to load class: " + fullQualifiedClassName + " because of exception [" + e.getMessage() + "]. Thus ignoring this class.", e);
					}
				}
			}
		}
		return classes;
	}

	/**
	 * Return a class's file name without '.class' extension. E.g. : dk/sebpension/MyClass.class
	 * will return dk/sebpension/MyClass
	 *
	 * @param fileName
	 * @return return class's file name without '.class' extension.
	 */
	private String getClassFileNameWithoutExtension(String fileName) {
		return fileName.substring(0, fileName.length() - CLASS_EXTENSION.length());
	}

	/**
	 * @param file
	 * @return true if the file end with '.jar', otherwise return false.
	 */
	private boolean isJarFile(final File file) {
		return file.toString().endsWith(JAR_EXTENSION);
	}
}
